package o.json;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

public class JSONReader {
	protected final Reader reader;

	public JSONReader(String text) {
		reader = new StringReader(text);
	}

	public JSONValue readValue() throws JSONException {
		try {
			if (testString("null")) {
				readString(4);
				return new JSONNull();
			}
			if (testString("true")) {
				readString(4);
				return new JSONTrue();
			}
			if (testString("false")) {
				readString(5);
				return new JSONFalse();
			}

			if (testString("\"")) {
				return readString();
			}
			if (testString("{")) {
				return readObject();
			}
			if (testString("[")) {
				return readArray();
			}

			if (testNumber()) {
				return readNumber();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		throw new JSONException("JSON value expected");
	}

	protected JSONString readString() throws JSONException {
		try {
			final StringBuffer buffer = new StringBuffer();

			reader.read(); // "
			for (int i = 0; true; ++i) {
				if (testString("\"")) {
					break;
				}

				final int data = reader.read();
				if (data < 0) throw new JSONException();

				char c = (char) data;
				if (c == '"') {
					break;
				} else {
					buffer.append(c);
				}
			}
			reader.read(); // "

			return new JSONString(buffer.toString());
		} catch (IOException e) {
			e.printStackTrace();
		}
		throw new JSONException();
	}

	private JSONNumber readNumber() throws JSONException {
		try {
			int number = 0;
			double decimal = 0;
			boolean isDecimal = false;
			boolean isNegative = false;
			if (testString("-")) {
				reader.read();
				isNegative = true;
			}

			for (int i = 0; true; ++i) {
				if (!isDigit()) break;

				final int digit = readDigit();
				number *= 10;
				number += digit;
			}
			if (testString(".")) {
				reader.read(); // .
				isDecimal = true;

				for (int i = 1; true; ++i) {
					if (!isDigit()) break;

					final int digit = readDigit();
					decimal += digit / (10*i);
				}
			}

			if (isNegative) {
				number = -number;
				decimal = -decimal;
			}

			if (isDecimal) {
				return new JSONNumber(number + decimal);
			} else {
				return new JSONNumber(number);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		throw new JSONException();
	}

	protected JSONObject readObject() throws JSONException {
		try {
			final JSONObject object = new JSONObject();

			reader.read(); // {
			for (int i = 0; true; ++i) {
				if (testString("}")) {
					break;
				}

				if (i != 0) {
					if (testString(",")) {
						reader.read(); // ,
					} else {
						throw new JSONException("expected ','");
					}
				}

				final JSONString key = readString();
				if (testString(":")) {
					reader.read(); // :
				} else {
					throw new JSONException("expected ':'");
				}
				final JSONValue value = readValue();
				object.setValue(key, value);
			}
			reader.read(); // }

			return object;
		} catch (IOException e) {
			e.printStackTrace();
		}
		throw new JSONException();
	}

	protected JSONArray readArray() throws JSONException {
		try {
			final JSONArray array = new JSONArray();

			reader.read(); // [
			for (int i = 0; true; ++i) {
				if (testString("]")) {
					break;
				}

				if (i != 0) {
					if (testString(",")) {
						reader.read();
					} else {
						throw new JSONException();
					}
				}

				final JSONValue value = readValue();
				array.addValue(value);
			}
			reader.read(); // ]

			return array;
		} catch (IOException e) {
			e.printStackTrace();
		}
		throw new JSONException();
	}

	private boolean testNumber() throws JSONException {
		try {
			try {
				reader.mark(2);
				if (testString("-")) {
					return true;
				}
				if (isDigit()) {
					return true;
				}
				return false;
			} finally {
				reader.reset();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		throw new JSONException();
	}

	private String readString(int length) throws IOException {
		if (length < 1) throw new IllegalArgumentException();

		char[] data = new char[length];
		int len = reader.read(data);
		if (len < length) throw new IOException();

		return new String(data);
	}
	private String peekString(int length) throws IOException {
		try {
			reader.mark(length);
			return readString(length);
		} finally {
			reader.reset();
		}
	}
	protected boolean testString(String text) {
		try {
			return text.equalsIgnoreCase(peekString(text.length()));
		} catch (IOException e) {
			return false;
		}
	}

	private int readDigit() throws IOException, JSONException {
		char[] data = new char[1];
		reader.read(data);

		final int digit = data[0] - '0';

		if (digit < 0) throw new JSONException();
		if (digit > 9) throw new JSONException();

		return digit;
	}
	private int peekDigit() throws IOException, JSONException {
		try {
			reader.mark(1);
			return readDigit();
		} finally {
			reader.reset();
		}
	}
	private boolean isDigit() {
		return isDigit(0, 9);
	}
	private boolean isDigit(int min, int max) {
		try {
			final int digit = peekDigit();
			if (digit < min) return false;
			if (digit > max) return false;
			return true;
		} catch (JSONException e) {
			return false;
		} catch (IOException e) {
			return false;
		}
	}
}
